[iOS 10] UICollectionViewにおけるセルのライフサイクルの変更とPre-Fetching APIによるスムーズなスクロールについて
1 はじめに
今回は、WWDC16のセッション219 What's New in UICollectionView in iOS 10の中で、UICollectionViewのセルのスムーズなスクロールについて解説されていたものを纏めてみました。
セッションの内容は、次の3つでしたが、そのうちの1つ目のトピックについてです。
- Smooth scrolling
- Improvements to self-sizing cells
- Interactive reordering
2 改善による効果
以下の図は、セッション中のデモの画像です。
画面の下部のグラフは、時間の経過とCPUの使用率をグラフにしたもので、赤いゾーンに入るぐらいCPUの負荷が集中した場合、セルの生成が間に合わなくなり、高速なスクロールで画面に「ちらつき」が生じる事を説明した際の資料です。
(1) iOS 9までの状況
スクロールを早く行うと、次のセルが画面に表示されるタイミングで、セルの再生成の処理が集中するため、CPUの使用率が赤いゾーンに突入しています。(画面が「ちらつく」)しかし、決して、CPUがフルに働いている訳ではではありません。
(2) iOS 10での状況
CPU使用率が分散されたため、赤いゾーンに突入することはなくなった。(画面の「ちらつき」は起こりにくくなった)
(3) iOS 10でPre-Fetching APIを導入した場合
Pre-Fetching APIを導入すると、メインスレッド以外への処理の分散によって、更に使用率が低減し、もはや、スクロールに「ちらつき」が生じることは無くなりました。
3 iOS 9までのライフサイクル
ここで、既存のセルのライフサイクルについて、再確認しておきます。
(1) prepareForReuse
非表示となって必要なくなったセルが再利用される場合、prepareForReuseが呼び出されます。 ここで、セルを初期化する事ができます。
(2) cellForItemAtIndexPath
データソースでセルを返すために、cellForItemAtIndexPathがoverrideされます。ここで、セルの生成の殆どの作業を行われます。
(3) willDisplayCell
セルを表示しようとする直前に呼び出されます。 セル表示の直前で必要な作業があれば、ここで行うことができます。
(4) didEndDisplayingCell
セルが表示領域外になった時は、didEndDisplayingCellが呼ばれます。
4 iOS 10における改善
iOS 10において変更されたセルのライフサイクルは、以下の2点です。
- cellForItemAtIndexPathを早くする
- didEndDisplayingCellを遅らせる
破棄が遅くなって、生成が早くなったため、当然の如く保持するセルの数は、増加しています。
(1) cellForItemAtIndexPath
cellForItemAtIndexPathの呼び出しが早くなり、 willDisplayCellまでの時間に余裕が出来ました。
(2) didEndDisplayingCell
従来、表示領域を超えて直ちに破棄されていたセルが、しばらく残ることになりました。
例えば、高速にスクロールしている時は、興味あるセルを見つけても、止めるのが間に合わなくて、そのセルが画面外に行ってしまったような場合、直ぐに逆方向に戻せば、まだ、そのセルは破棄されていないので、改めてセルを生成する必要が無いため、スムーズなスクロールに貢献することになります。
ここまでの変更に関しては、アプリ開発者はスムーズなスクロールのために、特に何も対応する必要が有りません。iOS 10でコンパイルし直すだけで、この効果を得ることが出来ます。
ただし、各メソッドが呼び出されるタイミングが変化していることは、意識しておくべきでしょう。また、保持するセル数が多くなて、当然メモリ消費量も上がっている事も忘れてはいけません。
5 Pre-Fetching APIにおける改善
上記のようにライフサイクルが改善されても、cellForItemAtIndexPathにおけるセルの生成で、DBアクセスやネットワークアクセスなど、負荷の高いモデルにアクセスが必要な場合は、対応に限界があります。
(1) UICollectionViewDataSourcePrefetching
そこで、iOS 10では、更なるスムーズなスクロールのために、従来のdelegate及びdata sourceに加えて、UICollectionViewDataSourcePrefetchingというプロトコルが追加されました。
このプロトコルにある、prefetchItemsAtは、従来、セルの生成の大半を担っていた、cellForItemAtIndexPathより、更に前に呼び出され、リソース取得などのセル生成の準備を早くから始めることが可能になります。また、メインスレッド以外で処理されているため、よりUIに影響を与えません。
なお、このprefetchItemsAtを使用する場合に必要な注意事項は、didEndDisplayCellやwillDisplayCellでの作業を最小限にしなければなりません。
protocol UICollectionViewDataSourcePrefetching { func collectionView(_ collectionView: UICollectionView, prefetchItemsAt indexPaths: [NSIndexPath]) optional func collectionView(_ collectionView: UICollectionView, cancelPrefetchingForItemsAt indexPaths: [NSIndexPath]) } class UICollectionView : UIScrollView { weak var prefetchDataSource: UICollectionViewDataSourcePrefetching? var isPrefetchingEnabled: Bool }
また、オプションで用意されているcancelPrefetchingForItemsAtは、現在進行中のスクロール方向が変化した時に呼ばれます。
先読みの必要が無くなるわけですから、このメソッドを利用して、先行していたデータモデルへのアクセスをキャンセルし、無駄な負荷を軽減することが出来ます。
(2) UITableViewDataSourcePrefetching
このPre-Fetching APIの仕組みは、UITableViewにも同じように追加されています。
rotocol UITableViewDataSourcePrefetching { func tableView(_ tableView: UITableView, prefetchRowsAt indexPaths: [NSIndexPath]) optional func tableView(_ tableView: UITableView, cancelPrefetchingForRowsAt indexPaths:[NSIndexPath]) } class UITableView : UIScrollView { weak var prefetchDataSource: UITableViewDataSourcePrefetching? }
6 最後に
今回、UICollectionViewにおけるセルのライフサイクルの改善について纏めてみました。
特に、Pre-Fetching APIをうまく利用することで、ネットワークアクセスなどが必要なデータを表示する場合も、非常に快適なスクロールを提供できるようになりそうな予感がします。
7 参考リンク
219 What's New in UICollectionView in iOS 10
API Reference UICollectionViewDataSourcePrefetching
API reference prefetchDataSource